home *** CD-ROM | disk | FTP | other *** search
/ Mac Easy 2010 May / Mac Life Ubuntu.iso / casper / filesystem.squashfs / usr / share / pyshared / apport / crashdb.py < prev    next >
Encoding:
Python Source  |  2009-04-06  |  18.2 KB  |  475 lines

  1. '''Abstract crash database interface.
  2.  
  3. Copyright (C) 2007 Canonical Ltd.
  4. Author: Martin Pitt <martin.pitt@ubuntu.com>
  5.  
  6. This program is free software; you can redistribute it and/or modify it
  7. under the terms of the GNU General Public License as published by the
  8. Free Software Foundation; either version 2 of the License, or (at your
  9. option) any later version.  See http://www.gnu.org/copyleft/gpl.html for
  10. the full text of the license.
  11. '''
  12.  
  13. import os, os.path, datetime, sys
  14.  
  15. from packaging_impl import impl as packaging
  16.  
  17. class CrashDatabase:
  18.     def __init__(self, auth_file, bugpattern_baseurl, options):
  19.         '''Initialize crash database connection. 
  20.         
  21.         You need to specify an implementation specific file with the
  22.         authentication credentials for retracing access for download() and
  23.         update(). For upload() and get_comment_url() you can use None.
  24.         
  25.         options is a dictionary with additional settings from crashdb.conf; see
  26.         get_crashdb() for details'''
  27.  
  28.         self.auth_file = auth_file
  29.         self.options = options
  30.         self.bugpattern_baseurl = bugpattern_baseurl
  31.         self.duplicate_db = None
  32.  
  33.     def get_bugpattern_baseurl(self):
  34.         '''Return the base URL for bug patterns.
  35.  
  36.         See apport.report.Report.search_bug_patterns() for details. If this
  37.         function returns None, bug patterns are disabled.'''
  38.  
  39.         return self.bugpattern_baseurl
  40.  
  41.     #
  42.     # API for duplicate detection
  43.     #
  44.     # Tests are in apport/crashdb_impl/memory.py.
  45.  
  46.     def init_duplicate_db(self, path):
  47.         '''Initialize duplicate database.
  48.  
  49.         path specifies an SQLite database. It will be created if it does not
  50.         exist yet.'''
  51.  
  52.         import sqlite3 as dbapi2
  53.  
  54.         assert dbapi2.paramstyle == 'qmark', \
  55.             'this module assumes qmark dbapi parameter style'
  56.  
  57.         init = not os.path.exists(path) or path == ':memory:' or \
  58.             os.path.getsize(path) == 0
  59.         self.duplicate_db = dbapi2.connect(path, timeout=7200)
  60.  
  61.         if init:
  62.             cur = self.duplicate_db.cursor()
  63.             cur.execute('''CREATE TABLE crashes (
  64.                 signature VARCHAR(255) NOT NULL,
  65.                 crash_id INTEGER NOT NULL,
  66.                 fixed_version VARCHAR(50),
  67.                 last_change TIMESTAMP)''')
  68.  
  69.             cur.execute('''CREATE TABLE consolidation (
  70.                 last_update TIMESTAMP)''')
  71.             cur.execute('''INSERT INTO consolidation VALUES (CURRENT_TIMESTAMP)''')
  72.             self.duplicate_db.commit()
  73.  
  74.         # verify integrity
  75.         cur = self.duplicate_db.cursor()
  76.         cur.execute('PRAGMA integrity_check');
  77.         result = cur.fetchall() 
  78.         if result != [('ok',)]:
  79.             raise SystemError, 'Corrupt duplicate db:' + str(result)
  80.  
  81.     def check_duplicate(self, id, report=None):
  82.         '''Check whether a crash is already known.
  83.  
  84.         If the crash is new, it will be added to the duplicate database and the
  85.         function returns None. If the crash is already known, the function
  86.         returns a pair (crash_id, fixed_version), where fixed_version might be
  87.         None if the crash is not fixed in the latest version yet. Depending on
  88.         whether the version in report is smaller than/equal to the fixed
  89.         version or larger, this calls close_duplicate() or mark_regression().
  90.         
  91.         If the report does not have a valid crash signature, this function does
  92.         nothing and just returns None.
  93.         
  94.         By default, the report gets download()ed, but for performance reasons
  95.         it can be explicitly passed to this function if it is already available.'''
  96.  
  97.         assert self.duplicate_db, 'init_duplicate_db() needs to be called before'
  98.  
  99.         if not report:
  100.             report = self.download(id)
  101.  
  102.         self._mark_dup_checked(id, report)
  103.  
  104.         sig = report.crash_signature()
  105.         if not sig:
  106.             return None
  107.  
  108.         existing = self._duplicate_search_signature(sig)
  109.  
  110.         # sort existing in ascending order, with unfixed last, so that
  111.         # version comparisons find the closest fix first
  112.         def cmp(x, y):
  113.             if x == y:
  114.                 return 0
  115.             if x == '':
  116.                 if y == None:
  117.                     return -1
  118.                 else:
  119.                     return 1
  120.             if y == '':
  121.                 if x == None:
  122.                     return 1
  123.                 else:
  124.                     return -1
  125.             if x == None:
  126.                 return 1
  127.             if y == None:
  128.                 return -1
  129.             return packaging.compare_versions(x, y)
  130.  
  131.         existing.sort(cmp, lambda k: k[1])
  132.  
  133.         if not existing:
  134.             # add a new entry
  135.             cur = self.duplicate_db.cursor()
  136.             cur.execute('INSERT INTO crashes VALUES (?, ?, ?, CURRENT_TIMESTAMP)', (sig, id, None))
  137.             self.duplicate_db.commit()
  138.             return None
  139.  
  140.         try:
  141.             report_package_version = report['Package'].split()[1]
  142.         except (KeyError, IndexError):
  143.             report_package_version = None
  144.  
  145.         # search the newest fixed id or an unfixed id to check whether there is
  146.         # a regression (crash happening on a later version than the latest
  147.         # fixed one)
  148.         for (ex_id, ex_ver) in existing:
  149.             if not ex_ver or \
  150.                not report_package_version or \
  151.                 packaging.compare_versions(report_package_version, ex_ver) < 0: 
  152.                 self.close_duplicate(id, ex_id)
  153.                 break
  154.         else:
  155.             # regression, mark it as such in the crash db
  156.             self.mark_regression(id, ex_id)
  157.  
  158.             # create a new record
  159.             cur = self.duplicate_db.cursor()
  160.             cur.execute('INSERT INTO crashes VALUES (?, ?, ?, CURRENT_TIMESTAMP)', (sig, id, None))
  161.             self.duplicate_db.commit()
  162.  
  163.         return (ex_id, ex_ver)
  164.  
  165.     def duplicate_db_fixed(self, id, version):
  166.         '''Mark given crash ID as fixed in the duplicate database.
  167.         
  168.         version specifies the package version the crash was fixed in (None for
  169.         'still unfixed').'''
  170.  
  171.         assert self.duplicate_db, 'init_duplicate_db() needs to be called before'
  172.  
  173.         cur = self.duplicate_db.cursor()
  174.         n = cur.execute('UPDATE crashes SET fixed_version = ?, last_change = CURRENT_TIMESTAMP WHERE crash_id = ?',
  175.             (version, id))
  176.         assert n.rowcount == 1
  177.         self.duplicate_db.commit()
  178.  
  179.     def duplicate_db_remove(self, id):
  180.         '''Remove crash from the duplicate database (because it got rejected or
  181.         manually duplicated).'''
  182.  
  183.         assert self.duplicate_db, 'init_duplicate_db() needs to be called before'
  184.  
  185.         cur = self.duplicate_db.cursor()
  186.         cur.execute('DELETE FROM crashes WHERE crash_id = ?', [id])
  187.         self.duplicate_db.commit()
  188.  
  189.     def duplicate_db_consolidate(self):
  190.         '''Update the duplicate database status to the reality of the crash
  191.         database.
  192.         
  193.         This uses get_unfixed() and get_fixed_version() to get the status of
  194.         particular crashes. Invalid IDs get removed from the duplicate db, and
  195.         crashes which got fixed since the last run are marked as such in the
  196.         database.
  197.  
  198.         This is a very expensive operation and should not be used too often.'''
  199.  
  200.         assert self.duplicate_db, 'init_duplicate_db() needs to be called before'
  201.  
  202.         unfixed = self.get_unfixed()
  203.  
  204.         cur = self.duplicate_db.cursor()
  205.         cur.execute('SELECT crash_id, fixed_version FROM crashes')
  206.  
  207.         cur2 = self.duplicate_db.cursor()
  208.         for (id, ver) in cur:
  209.             # crash got reopened
  210.             if id in unfixed:
  211.                 if ver != None:
  212.                     cur2.execute('UPDATE crashes SET fixed_version = NULL, last_change = CURRENT_TIMESTAMP WHERE crash_id = ?', [id])
  213.                 continue
  214.  
  215.             if ver != None:
  216.                 continue # skip get_fixed_version(), we already know its fixed
  217.  
  218.             # crash got fixed/rejected
  219.             fixed_ver = self.get_fixed_version(id)
  220.             if fixed_ver == 'invalid':
  221.                 print 'DEBUG: bug %i was invalidated, removing from database' % id
  222.                 cur2.execute('DELETE FROM crashes WHERE crash_id = ?', [id])
  223.             elif not fixed_ver:
  224.                 print 'WARNING: inconsistency detected: bug #%i does not appear in get_unfixed(), but is not fixed yet' % id
  225.             else:
  226.                 cur2.execute('UPDATE crashes SET fixed_version = ?, last_change = CURRENT_TIMESTAMP WHERE crash_id = ?',
  227.                     (fixed_ver, id))
  228.  
  229.         # poke consolidation.last_update
  230.         cur.execute('UPDATE consolidation SET last_update = CURRENT_TIMESTAMP')
  231.         self.duplicate_db.commit()
  232.  
  233.     def duplicate_db_last_consolidation(self, absolute=False):
  234.         '''Return the date and time of last consolidation.
  235.         
  236.         By default, this returns the number of seconds since the last
  237.         consolidation. If absolute is True, the date and time of last
  238.         consolidation will be returned as a string instead.'''
  239.  
  240.         assert self.duplicate_db, 'init_duplicate_db() needs to be called before'
  241.  
  242.         cur = self.duplicate_db.cursor()
  243.         cur.execute('SELECT last_update FROM consolidation')
  244.         if absolute:
  245.             return cur.fetchone()[0]
  246.         else:
  247.             last_run = datetime.datetime.strptime(cur.fetchone()[0], '%Y-%m-%d %H:%M:%S')
  248.             delta = (datetime.datetime.utcnow() - last_run)
  249.             return delta.days * 86400 + delta.seconds
  250.  
  251.     def duplicate_db_needs_consolidation(self, interval=86400):
  252.         '''Check whether the last duplicate_db_consolidate() happened more than
  253.         'interval' seconds ago (default: one day).'''
  254.  
  255.         return self.duplicate_db_last_consolidation() >= interval
  256.  
  257.     def duplicate_db_change_master_id(self, old_id, new_id):
  258.         '''Change a crash ID.'''
  259.  
  260.         assert self.duplicate_db, 'init_duplicate_db() needs to be called before'
  261.  
  262.         cur = self.duplicate_db.cursor()
  263.         cur.execute('UPDATE crashes SET crash_id = ?, last_change = CURRENT_TIMESTAMP WHERE crash_id = ?',
  264.             [new_id, old_id])
  265.         self.duplicate_db.commit()
  266.  
  267.     def _duplicate_search_signature(self, sig):
  268.         '''Look up signature in the duplicate db and return an [(id,
  269.         fixed_version)] tuple list.
  270.         
  271.         There might be several matches if a crash has been reintroduced in a
  272.         later version.'''
  273.  
  274.         cur = self.duplicate_db.cursor()
  275.         cur.execute('SELECT crash_id, fixed_version FROM crashes WHERE signature = ?', [sig])
  276.         return cur.fetchall()
  277.  
  278.     def _duplicate_db_dump(self, with_timestamps=False):
  279.         '''Return the entire duplicate database as a dictionary signature ->
  280.            (crash_id, fixed_version).
  281.  
  282.            If with_timestamps is True, then the map will contain triples
  283.            (crash_id, fixed_version, last_change) instead.
  284.  
  285.            This is mainly useful for debugging and test suites.'''
  286.  
  287.         assert self.duplicate_db, 'init_duplicate_db() needs to be called before'
  288.  
  289.         dump = {}
  290.         cur = self.duplicate_db.cursor()
  291.         cur.execute('SELECT * FROM crashes')
  292.         for (sig, id, ver, last_change) in cur:
  293.             if with_timestamps:
  294.                 dump[sig] = (id, ver, last_change)
  295.             else:
  296.                 dump[sig] = (id, ver)
  297.         return dump
  298.  
  299.     #
  300.     # Abstract functions that need to be implemented by subclasses
  301.     #
  302.  
  303.     def upload(self, report, progress_callback = None):
  304.         '''Upload given problem report return a handle for it. 
  305.         
  306.         This should happen noninteractively. 
  307.         
  308.         If the implementation supports it, and a function progress_callback is
  309.         passed, that is called repeatedly with two arguments: the number of
  310.         bytes already sent, and the total number of bytes to send. This can be
  311.         used to provide a proper upload progress indication on frontends.'''
  312.  
  313.         raise NotImplementedError, 'this method must be implemented by a concrete subclass'
  314.  
  315.     def get_comment_url(self, report, handle):
  316.         '''Return an URL that should be opened after report has been uploaded
  317.         and upload() returned handle.
  318.  
  319.         Should return None if no URL should be opened (anonymous filing without
  320.         user comments); in that case this function should do whichever
  321.         interactive steps it wants to perform.'''
  322.  
  323.         raise NotImplementedError, 'this method must be implemented by a concrete subclass'
  324.  
  325.     def download(self, id):
  326.         '''Download the problem report from given ID and return a Report.'''
  327.  
  328.         raise NotImplementedError, 'this method must be implemented by a concrete subclass'
  329.  
  330.     def update(self, id, report, comment):
  331.         '''Update the given report ID with the retraced results from the report
  332.         (Stacktrace, ThreadStacktrace, StacktraceTop; also Disassembly if
  333.         desired) and an optional comment.'''
  334.  
  335.         raise NotImplementedError, 'this method must be implemented by a concrete subclass'
  336.  
  337.     def get_distro_release(self, id):
  338.         '''Get 'DistroRelease: <release>' from the given report ID and return
  339.         it.'''
  340.  
  341.         raise NotImplementedError, 'this method must be implemented by a concrete subclass'
  342.  
  343.     def get_unretraced(self):
  344.         '''Return an ID set of all crashes which have not been retraced yet and
  345.         which happened on the current host architecture.'''
  346.  
  347.         raise NotImplementedError, 'this method must be implemented by a concrete subclass'
  348.  
  349.     def get_dup_unchecked(self):
  350.         '''Return an ID set of all crashes which have not been checked for
  351.         being a duplicate.
  352.  
  353.         This is mainly useful for crashes of scripting languages such as
  354.         Python, since they do not need to be retraced. It should not return
  355.         bugs that are covered by get_unretraced().'''
  356.  
  357.         raise NotImplementedError, 'this method must be implemented by a concrete subclass'
  358.  
  359.     def get_unfixed(self):
  360.         '''Return an ID set of all crashes which are not yet fixed.
  361.  
  362.         The list must not contain bugs which were rejected or duplicate.
  363.         
  364.         This function should make sure that the returned list is correct. If
  365.         there are any errors with connecting to the crash database, it should
  366.         raise an exception (preferably IOError).'''
  367.  
  368.         raise NotImplementedError, 'this method must be implemented by a concrete subclass'
  369.  
  370.     def get_fixed_version(self, id):
  371.         '''Return the package version that fixes a given crash.
  372.  
  373.         Return None if the crash is not yet fixed, or an empty string if the
  374.         crash is fixed, but it cannot be determined by which version. Return
  375.         'invalid' if the crash report got invalidated, such as closed a
  376.         duplicate or rejected.
  377.  
  378.         This function should make sure that the returned result is correct. If
  379.         there are any errors with connecting to the crash database, it should
  380.         raise an exception (preferably IOError).'''
  381.  
  382.         raise NotImplementedError, 'this method must be implemented by a concrete subclass'
  383.  
  384.     def duplicate_of(self, id):
  385.         '''Return master ID for a duplicate bug.
  386.  
  387.         If the bug is not a duplicate, return None.
  388.         '''
  389.         raise NotImplementedError, 'this method must be implemented by a concrete subclass'
  390.  
  391.     def close_duplicate(self, id, master):
  392.         '''Mark a crash id as duplicate of given master ID.
  393.         
  394.         If master is None, id gets un-duplicated.
  395.         '''
  396.         raise NotImplementedError, 'this method must be implemented by a concrete subclass'
  397.  
  398.     def mark_regression(self, id, master):
  399.         '''Mark a crash id as reintroducing an earlier crash which is
  400.         already marked as fixed (having ID 'master').'''
  401.         
  402.         raise NotImplementedError, 'this method must be implemented by a concrete subclass'
  403.  
  404.     def mark_retraced(self, id):
  405.         '''Mark crash id as retraced.'''
  406.  
  407.         raise NotImplementedError, 'this method must be implemented by a concrete subclass'
  408.  
  409.     def mark_retrace_failed(self, id, invalid_msg=None):
  410.         '''Mark crash id as 'failed to retrace'.
  411.  
  412.         If invalid_msg is given, the bug should be closed as invalid with given
  413.         message, otherwise just marked as a failed retrace.
  414.         
  415.         This can be a no-op if you are not interested in this.'''
  416.  
  417.         raise NotImplementedError, 'this method must be implemented by a concrete subclass'
  418.  
  419.     def _mark_dup_checked(self, id, report):
  420.         '''Mark crash id as checked for being a duplicate
  421.         
  422.         This is an internal method that should not be called from outside.'''
  423.  
  424.         raise NotImplementedError, 'this method must be implemented by a concrete subclass'
  425. #
  426. # factory 
  427. #
  428.  
  429. def get_crashdb(auth_file, name = None, conf = None):
  430.     '''Return a CrashDatabase object for the given crash db name, as specified
  431.     in the configuration file 'conf'.
  432.     
  433.     If name is None, it defaults to the 'default' value in conf.
  434.  
  435.     If conf is None, it defaults to the environment variable
  436.     APPORT_CRASHDB_CONF; if that does not exist, the hardcoded default is
  437.     /etc/apport/crashdb.conf. This Python syntax file needs to specify:
  438.  
  439.     - A string variable 'default', giving a default value for 'name' if that is
  440.       None.
  441.  
  442.     - A dictionary 'databases' which maps names to crash db configuration
  443.       dictionaries. These need to have at least the keys 'impl' (Python module
  444.       in apport.crashdb_impl which contains a concrete 'CrashDatabase' class
  445.       implementation for that crash db type) and 'bug_pattern_base', which
  446.       specifies an URL for bug patterns (or None if those are not used for that
  447.       crash db).'''
  448.  
  449.     if not conf:
  450.         conf = os.environ.get('APPORT_CRASHDB_CONF', '/etc/apport/crashdb.conf')
  451.     settings = {}
  452.     execfile(conf, settings)
  453.  
  454.     # Load third parties crashdb.conf
  455.     confdDir = conf + '.d'
  456.     if os.path.isdir(confdDir):
  457.         for cf in os.listdir(confdDir):
  458.             cfpath = os.path.join(confdDir, cf)
  459.             if os.path.isfile(cfpath) and cf.endswith('.conf'):
  460.                 try:
  461.                     execfile(cfpath, settings['databases'])
  462.                 except Exception, e:
  463.                     # ignore broken files
  464.                     print >> sys.stderr, 'Invalid file %s: %s' % (cfpath, str(e))
  465.                     pass
  466.  
  467.     if not name:
  468.         name = settings['default']
  469.  
  470.     db = settings['databases'][name]
  471.  
  472.     m = __import__('apport.crashdb_impl.' + db['impl'], globals(), locals(), ['CrashDatabase'])
  473.     return m.CrashDatabase(auth_file, db['bug_pattern_base'], db)
  474.  
  475.